from __future__ import absolute_import
import itertools
import logging
import os
import shutil
import urlgrabber.progress
from pkgr.errors import RPMError
from pkgr.package import InstalledPackage
from pkgr.package import RemotePackage
from pkgr.package import Requires
from pkgr.repository.repository import Repository
from pkgr.rpm.database import RPMDatabase
from pkgr.transaction import Transaction
from pkgr.util import mkdir_p
from pkgr.util import parse_package_name
from urlgrabber.grabber import URLGrabber, URLGrabError

logger = logging.getLogger('pkgr.pkgr')

class Pkg(object):
    _config = None
    # caches a readonly RPMDatabase
    _rpmdb_ro = None
    # caches a r/w RPMDatabase
    _rpmdb = None
    _repos = None
    
    def __init__(self):
        self.reset_repos()
        self.reset_transaction()
        self.set_progress_meter(urlgrabber.progress.text_progress_meter())
        
    def reset_transaction(self):
        self._trans = Transaction()
        if self._rpmdb:
            self._rpmdb.close()
            self._rpmdb = None
        
    def get_transaction(self):
        return self._trans
        
    def set_config(self, config):
        self._config = config
        
        # build the repos here so we can replace vars
        for repo in config.get_repos():
            self.add_repo(repo)
        
    def get_config(self):
        if self._config is None:
            raise Exception("Pkg requires a configuration object with set_config().")
            
        return self._config
     
    def what_provides(self, requirement):
        """ Returns: The latest package that provides requirement.
        
            Args:
                requirement(:class:`~pkgr.package.Requires`): A Requires instance.
        """
        for repo in self.get_repos():
            for package in repo.database.find_packages_providing(requirement):
                yield package
      
    def get_latest_package(self, packages):
        """ Given a list of packages with the same name return the one that is latest.
        """
        # TODO: if inconsistent names in packages, return the best of the bunch
        latest_provide = None
        for package in packages:
            if latest_provide is None:
                latest_provide = package
                continue
            if package.get_name() != latest_provide.get_name():
                continue
                #raise Exception("Inconsistant package list %s" % packages)
            if package > latest_provide:
                latest_provide = package
        return latest_provide
        
        
    def get_best_packages(self, packages):
        # returns the best of same named packages in a list of packages
        pkgmap = {}
        for package in packages:
            package_name = package.get_name()
            if package_name not in pkgmap:
                pkgmap[package_name] = package
            else:
                if package > pkgmap[package_name]:
                    pkgmap[package_name] = package
        ret = pkgmap.values()
        #logger.info("Best packages of %s: %s", packages, ret)
        return ret
        
    _copy_local = True
    def set_copylocal(self, enabled):
        """ Sets f packages should be copied to the rpm cache if they are
            already local.
        """
        self._copy_local = enabled
        
    def get_copylocal(self):
        """ Returns:
                True if packages should be copied to the rpm cache if they are
                already local.
        """
        return self._copy_local
        
    def _download(self, url, filename=None):
        prog = self.get_progress_meter()
        g = URLGrabber(progress_obj=prog)
        return g.urlgrab(url, filename=filename, copy_local=1 if self.get_copylocal() else 0)
        
    def set_progress_meter(self, m):
        """ Sets the progress meter to use.
        
            Args:
                m(urlgrabber.progress.BaseMeter): A progress meter.
        """
        self._progress_meter = m

    def get_progress_meter(self):
        """ Returns the progress meter. By default returns a
            urlgrabber.progress.text_progress_meter
        """
        return self._progress_meter
        
    def add_repo(self, r):
        """ Adds a repository to use to search for packages/provides.
            
            Args:
                r(:class:`~pkgr.repository.repository.Repository`): The 
                repository to add.
        """
        self.get_repos().add(r)
        
    def reset_repos(self):
        """ Empties the set of repos.
        """
        self._repos = set()
        
    def get_repos(self):
        """ Returns the set of repos.
        """
        return self._repos
        
    def var_replace(self, string):
        """ Replaces config variables in the string.
        """
        return string.replace('$release', self.get_config().get_release()).\
        replace('$architecture', self.get_config().get_arch())
        
    def _init_cache_dir(self):
        cachedir = self.get_config().get_cachedir()
        logger.info('Initializing cache directory %s', cachedir)
        if not os.path.exists(cachedir):
            mkdir_p(cachedir)
            
    def init_repo_md(self):
        """ Initializes any repository metadata requiring initializing.
        """
        self._init_cache_dir()
        for repo in self.get_repos():
            cachedir = self.get_config().join_cache_dir('repos', repo.get_name())
            repo.set_cachedir(cachedir)
            repo.init()
    
    def is_package_installed(self, package):
        """ Args:
                package (:class:`~pkgr.package.Package`): A package instance.
                
            Returns:
                True if package is installed.
        """
        r = Requires(package.get_name())
        return self.is_requirement_installed(r)
    
    def _get_rpmdb_readonly(self):
        if self._rpmdb_ro is None: 
            self._rpmdb_ro = RPMDatabase.readonly(self.get_config().get_installroot(), self.get_config().get_rpmdbpath())
        return self._rpmdb_ro
       
    def _get_rpmdb(self):
        if self._rpmdb is None: 
            self._rpmdb = RPMDatabase(self.get_config().get_installroot(), self.get_config().get_rpmdbpath())
        return self._rpmdb
        
    def get_installed_providing(self, requirement):
        """ Returns a set of packages providing requirement.
        """
        return self._get_rpmdb().get_installed_providing(requirement)
        
    def is_requirement_installed(self, requirement):
        """ Returns True if the Requires requirment is installed. 
        """
        #logger.debug('Check r is installed %s', requirement)
        return self._get_rpmdb().is_requirement_installed(requirement)
    def get_all_installed(self):
        """ TODO: return all installed packages. """
    def is_name_installed(self, name):
        """ Returns true if the name is installed. """
        r = Requires(name)
        return self._get_rpmdb().is_requirement_installed(r)
        
    def get_unsatified_requirements(self, package):
        """ Returns a list of Requires that are unsatified for a given package. """
        unsatified_reqs = []
        for requires in package.get_requires():
            if not self.is_requirement_installed(requires):
                unsatified_reqs.append(requires)
        return unsatified_reqs
    
    def repos_find_packages_providing(self, requirement):
        """ Returns the packages providing the requirement
        """
        packages = set()
        for repo in self.get_repos():
            for package in repo.database.find_packages_providing(requirement):
                if package.meets_requirement(requirement):
                    packages.add(package)
        return packages
        
    def repos_find_best_packages_providing(self, requirement):
        packages = []
        for repo in self.get_repos():
            for package in repo.database.find_packages_providing(requirement):
                packages.append(package)
        return self.get_best_packages(packages)
        
        
    def find_package(self, name):
        """ Returns the packages with the exact name. 
        """
        packages = []
        
        name, version, release, epoch, arch = parse_package_name(name)
        
        for repo in self.get_repos():
            for package in repo.database.search_package(name, version=version, release=release, epoch=epoch, arch=arch):
                packages.append(package)
                    
        return packages
        
    def search(self, package_or_group_spec):
        packages = {}
        for repo in self.get_repos():
            for package in repo.database.search_package_name_glob(package_or_group_spec):
                
                package_id = package.get_id()
                if package_id not in packages:
                    packages[package_id] = package
                else:
                    if package > packages[package_id]:
                        packages[package_id] = package
                        
        return sorted(packages.values(), key=lambda x: x.get_name())
        # uses the indexes to find a package or group matching
    def repo_search_name_and_desc(self, name_or_desc):
        packages = {}
        for repo in self.get_repos():
            for package in repo.database.search_package_name_desc_contains(name_or_desc):
                
                package_id = package.get_id()
                if package_id not in packages:
                    packages[package_id] = package
                else:
                    if package > packages[package_id]:
                        packages[package_id] = package
                        
        return sorted(packages.values(), key=lambda x: x.get_name())
    def search_groups_by_id(self, id):
        """ Searches repositories with groups enabled for a group matching id.
        """
        match = None
        for repo in self.get_repos():
            if repo.get_groups_enabled():
                search = repo.groups.search_by_id(id)
                if search:
                    match = search
        return match
      
    def get_all_groups(self):
        groups = []
        for repo in self.get_repos():
            if repo.get_groups_enabled():
                groups += repo.groups.get_groups()
        return groups
        
    def install_package(self, package):
        if not isinstance(package, RemotePackage):
            raise ValueError("package must be a RemotePackage instance.")
        self.get_transaction().add_install(package)
        
    def install_requirement(self, requirement):
        """ Checks if a requirement is installed. Finds it in the repos and adds
            it to the install if it isn't.
            
            Args:
                requirement (:class:`~pkgr.package.Requires`): A Requirement instance.
        """
        # check if anything providing the requirement is installed
        installed_providers = self.get_installed_providing(requirement)
        
        if len(installed_providers) > 0:
            logger.debug('Packages providing %s are already installed: %s', requirement, installed_providers)
            return False
        
        # find requirement in repos
        logger.debug('Finding best package providing %s', requirement)
        pkgs = self.repos_find_packages_providing(requirement)
        
        # filter the best
        latest = self.get_latest_package(pkgs)
        
        logger.debug('Adding to transaction: %s', latest)
        self.get_transaction().add_install(latest)
        return True
    
    def update_package(self, package):
        """ Adds a package to update.
            
            Args:
                package (:class:`~pkgr.package.RemotePackage`): The 
                package to update.
        """
        self.get_transaction().add_update(package)
        
    def remove(self, package_name):
        installed = self.get_installed_providing(Requires(package_name))
        if not installed:
            logger.debug('Requirement matching %s is not installed.', package_name)
            return
            
        if len(installed) > 1:
            raise ValueError("Ambiguous requires name %s" % package_name)
        self.remove_package(list(installed)[0])  
            
    def remove_package(self, package):
        """ Add a package to remove.
            
            Args:
                package (:class:`~pkgr.package.InstalledPackage`): The
                package to remove
        """
        self.get_transaction().add_remove(package)
        
    def resolve_deps(self):
        """ Attempts to resolve dependencies.
            
            This uses a topological sort algorithm over the package and their
            requires.
            
            Requirements are checked if they are installed. If they aren't, the
            repos are searched for a best provider, and the provider is added to
            the install/update list.
            
        """
        logger = logging.getLogger('pkgr.pkgr')
        transaction = self.get_transaction()
        
        packages = transaction.get_install()
        packages |= transaction.get_update()
        
        if not len(packages) and not len(transaction.get_remove()):
            logger.debug('No installs/updates/removes to resolve.')
            return
            
        logger.debug('Resolving deps for %s packages...', len(packages))
        pending = [(None, p) for p in packages]
        packagesrequiringinstall = set()
        packagesrequiringupdate = set()
        
        obsoletedby = {}
        obsoleted = set()
        seen = set()
        
        additional_deps = []
        
        
        unresolved_requirements = set()
        
        while len(pending):
            unresolved_requirements = set()
        
        
        requirements_checked = set()
        while len(pending):
            next_pending = set()
            
            for requiredby, package in pending:
                seen.add(package)
                
                for obsolete in package.get_obsoletes():
                    obsoleted.add(obsolete)
                    obsoletedby[obsolete] = package
                    
                packagesprovidingreqs = set()
                # check that all of the packages requirements are installed
                # if not, adds them to the pending queue
                for r in package.get_requires():
                    if r in requirements_checked:
                        continue
                        
                    if self.is_requirement_installed(r):
                        logger.log(5, 'Requirement %s already installed.', str(r))
                        continue
                        
                    # find the best provider
                    packages = self.repos_find_best_packages_providing(r)
                    
                    if not packages:
                        raise Exception("No package found providing %s" % r)
                    
                    packagesprovidingreqs.update(packages)
                    requirements_checked.add(r)
                    
                logger.info('!! Package %s requires: %s', package, packagesprovidingreqs)
                
                for latest in packagesprovidingreqs:
                    additional_deps.append(latest)
                    next_pending.add((package, latest))
                    
            pending = list(next_pending)
           
        for p in self.get_best_packages(additional_deps):
            # check if package is an update or an install
            is_update = self.is_package_installed(p)
            
            if is_update:
                logger.debug('%s added as dep update.', package)
                packagesrequiringupdate.add(p)
            else:
                logger.debug('%s added as dep install.', package)
                packagesrequiringinstall.add(p)
                
                
        if packagesrequiringinstall:
            logger.debug('Adding install packages to transaction: %s', packagesrequiringinstall)
            for package in packagesrequiringinstall:
                self.install_package(package)
         
        if packagesrequiringupdate:
            logger.debug('Adding update packages to transaction: %s', packagesrequiringinstall)
            for package in packagesrequiringupdate:
                self.update_package(package) 
       
        # copy the removes before we remove obsoletes
        pending = [(p, p) for p in transaction.get_remove()]
        
        if len(obsoleted):
            logger.debug('Obsoleted packages: %s', obsoleted)
            # remove any obsoleted packages that are installed
            for obsolete in obsoleted:
                if self.is_requirement_installed(obsolete):
                    logger.debug('Adding obsoleted package %s to removes. Obsoleted by %s', obsolete.get_name(), obsoletedby[obsolete])
                    self.remove_package(obsolete)
               
        # loop over the removes and recursively remove any packages requiring
        additional_removals = set()
        requires_checked = set()
        while len(pending):
            next_pending = set()
            
            for requiredby, package in pending:
                packages_requiring_removed_package = set()
                for prov in package.get_provides():
                    if prov in requires_checked:
                        continue
                    reqs = self.rpmdb_get_packages_requiring(prov)
                    packages_requiring_removed_package |= reqs
                    requires_checked.add(prov)
                
                logger.debug('Packages requiring %s: %s', package, packages_requiring_removed_package)#, str(list(package.get_provides(False))))
                for p in packages_requiring_removed_package:
                    # it is an error if and of the removes include any packages are being installed/updated in the same transaction
                    if p in packagesrequiringinstall or p in packagesrequiringupdate:
                        raise Exception("Package %s being installed/update is being removed by: %s" % (p, requiredby))
                    next_pending.add((package, p))
                    additional_removals.add(p)
                    
            pending = list(next_pending)
            
        for p in additional_removals:
            self.remove_package(p)
            
    def rpmdb_get_packages_requiring(self, r):
        return self._get_rpmdb().find_packages_requiring(r)
    
    def run(self):
        """ Runs the current transaction.
        
            1. downloads all packages.
            2. adds any obsoleted to remove
            3. adds any installs to install
            4. adds any updates to updates
            5. checks, orders and runs the transaction using a
               :class:`~pkgr.rpm.database.RPMDatabase`
        """
        
        somethingtodo = False
        
        if len(self.get_transaction().get_remove()):
            somethingtodo = True
        
        if len(self.get_transaction().get_install()):
            somethingtodo = True
            
        if len(self.get_transaction().get_update()):
            somethingtodo = True
            
        if not somethingtodo:
            return
            
        rpmcachepath = self.get_config().get_rpmcachedir()
        
        if not os.path.exists(rpmcachepath):
            mkdir_p(rpmcachepath)
            
        rpmdb = self._get_rpmdb()
        rpmdb.set_vs_flags(-1)
        
        obsoleted = set()
        
        logger.debug('Install: %s', str(self.get_transaction().get_install()))
        logger.debug('Update: %s', str(self.get_transaction().get_update()))
        logger.debug('Remove: %s', str(self.get_transaction().get_remove()))
        # check if all the requested RemotePackages are downloaded
        for package in itertools.chain(self.get_transaction().get_install(), self.get_transaction().get_update()):
            
            url = package.get_download_url()
            cachepath = os.path.join(rpmcachepath, os.path.basename(url))
            repo = package.get_repository()
            cur = repo.get_current_mirror()
            
            if os.path.exists(cachepath):
                logger.debug('Package %s cached at %s', package, cachepath)
                continue
            
            logger.debug('Downloading package %s', package)
            
            try:
                repomddata = self._download(url, cachepath)
            except URLGrabError, e:
                logger.exception(e)
                logger.error("Error downloading package %s" % url)
                logger.debug('Trying next mirrors %s', mirrorbase)
                # loop over all urls until we find a satisfactory repomd.xml
                for mirrorbase in repo.get_next_mirror():
                    try:
                        logger.debug('Trying next mirror %s', mirrorbase)
                        repomddata = self._download(url, cachepath)
                        break
                    except URLGrabError, e:
                        logger.exception(e)
                        logger.error("Error downloading package %s" % url)
        
        # handle removes
        for package in self.get_transaction().get_remove():
            logger.debug('Adding package %s to removes.', package.get_name())
            rpmdb.add_erase(package.get_name())
            
        # handle installs
        for package in self.get_transaction().get_install():
            cachepath = os.path.join(rpmcachepath, package.get_basename())
            logger.debug('Addding package %s (%s) to installs.', package, cachepath)
            rpmdb.add_install(package, cachepath)
         
        # handle updates
        for package in self.get_transaction().get_update():
            cachepath = os.path.join(rpmcachepath, package.get_basename())
            logger.debug('Addding package %s (%s) to updates.', package, cachepath)
            rpmdb.add_update(package, cachepath)
            
        unresolved_dependencies = rpmdb.check()
        
        if len(unresolved_dependencies) > 0:
            logger.warn('Unresolved deps: %s', unresolved_dependencies)
        
        logger.debug('Ordering transaction...')
        rpmdb.order()
        
        for te in rpmdb.get_ts():
            logger.debug('Transaction element: %s %s %s', te.N(), te.V(), te.R())
        
        logger.info("Running transaction...")
        
        res = rpmdb.run()
        
        logger.info('Completed running transaction. %s', str(res))
        
        if res is not None and len(res)>0:
            message = '\n'.join([err[0] for err in res])
            raise RPMError("Rpm returned message: %s" % message)
        pass
    
        self.reset_transaction()

